package com.mi.common.log.aspect;


import cn.hutool.core.text.StrPool;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgentUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mi.common.core.constant.CommonConstants;
import com.mi.common.core.util.SpELParserUtils;
import com.mi.common.log.annotation.Log;
import com.mi.common.log.constant.enums.LogTypeEmum;
import com.mi.common.log.event.LogEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;

import static cn.hutool.core.text.CharSequenceUtil.isBlank;

/**
 * 日志请求Api Logger Aspect
 *
 * @author jin_huaquan
 * @version 1.0
 * @date 2018/11/21 14:51
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LogAspect {

    private final ObjectMapper objectMapper;
    private final ApplicationContext applicationContext;

    private static final String LOGIN_URI = "/actions/login";

    private static final String HTTP_HEADER_USER_AGENT = "user-agent";
    private static final String HTTP_ATTRIBUTE_USERNAME = "username";
    private ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Pointcut("@annotation(com.mi.common.log.annotation.Log)")
    public void logPointcut() {
    }

    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        Object result = joinPoint.proceed();
        long executionTime = System.currentTimeMillis() - startTime.get();

        LogEvent.Log logEvent = setLog(joinPoint, result);

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        log.info("URL : {}", logEvent.getRequestUri());
        log.info("HTTP_METHOD : {}", logEvent.getHttpMethod());
        log.info("BROWSER : {}", logEvent.getUserAgent());
        log.info("REMOTE_ADDR : {}", logEvent.getRemoteAddr());
        log.info("CLASS_METHOD : {}", logEvent.getClassMethod());
        log.info("ARGS : {}", logEvent.getParams());

        log.info("EXECUTION TIME : {}", executionTime);
        log.info("RESPONSE : {}", objectMapper.writeValueAsString(result));

        log.info("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");

        logEvent.setExecutionTime(executionTime);
        logEvent.setType(LogTypeEmum.NORMAL.getValue());

        // publish log event
        applicationContext.publishEvent(new LogEvent(logEvent));

        return result;
    }

    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) throws Throwable {
        e.printStackTrace();
        LogEvent.Log log = setLog((ProceedingJoinPoint) joinPoint, null);
        String stackTrace = getStackTrace(e);

        log.setException(stackTrace);
        log.setExecutionTime(0L);
        log.setType(LogTypeEmum.ERROR.getValue());

        // publish log event
        applicationContext.publishEvent(new LogEvent(log));
    }

    /**
     * 从切面解析拼装出日志信息
     *
     * @param joinPoint 切面信息
     * @return 日志信息
     */
    private LogEvent.Log setLog(ProceedingJoinPoint joinPoint, Object result) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        Log aopLog = method.getAnnotation(Log.class);

        // 方法路径
        String classMethod = joinPoint.getTarget().getClass().getName() + StrPool.DOT + signature.getName() + "()";

        StringBuilder params = new StringBuilder("{");
        // 参数值
        Object[] argValues = joinPoint.getArgs();
        // 参数名称
        String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        if (argValues != null) {
            for (int i = 0; i < argValues.length; i++) {
                params.append(" ").append(argNames[i]).append(": ").append(argValues[i]);
            }
        }
        params.append("}");

        String uri = request.getRequestURI();

        String username;
        if (uri.endsWith(LOGIN_URI)) {
            username = objectMapper.valueToTree(argValues[0]).get(HTTP_ATTRIBUTE_USERNAME).asText();
        } else {
            username = (String) request.getAttribute(HTTP_ATTRIBUTE_USERNAME);
        }

        LogEvent.Log log = new LogEvent.Log();

        log.setTitle(SpELParserUtils.parse(method, argValues, result, aopLog.title(), String.class, aopLog.title()));
        log.setServiceId(aopLog.serviceId());
        log.setRemoteAddr(ServletUtil.getClientIP(request));
        log.setUsername(isBlank(username) ? CommonConstants.ANONYMOUS : username);
        log.setUserAgent(UserAgentUtil.parse(request.getHeader(HTTP_HEADER_USER_AGENT)).getBrowser().getName());
        log.setRequestUri(uri);
        log.setHttpMethod(request.getMethod());
        log.setClassMethod(classMethod);
        log.setParams(params.toString());

        return log;
    }

    /**
     * 获取堆栈信息
     */
    private static String getStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            throwable.printStackTrace(pw);
            return sw.toString();
        }
    }
}
